/****************************************************************************
 *   Copyright (C) 2008 by 7552                                             *
 *   fiubataller2-08c1@gruposyahoo.com.ar                                   *
 *                                                                          *
 *  Licensed under the Apache License, Version 2.0 (the "License");         *
 *  you may not use this file except in compliance with the License.        *
 *  You may obtain a copy of the License at                                 *
 *                                                                          *
 *      http://www.apache.org/licenses/LICENSE-2.0                          *
 *                                                                          *
 *  Unless required by applicable law or agreed to in writing, software     *
 *  distributed under the License is distributed on an "AS IS" BASIS,       *
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.*
 *  See the License for the specific language governing permissions and     *
 *  limitations under the License.                                          *
 *                                                                          *
 ***************************************************************************/

#include "LogCache.h"
#include <cstdio>
#include <iostream>

using namespace Taller2::TransRec;

const string DEFAULT_ROOT = "./";

LogCache::LogCache(const string &database)
{
	m_CurrentSize = 0;
	fileName = DEFAULT_ROOT + database + ".log";
	if (database == _DEFAULT_DB_)
		fileName = DEFAULT_ROOT + "oodb.log";
//cout<<fileName<<endl;
	// intenta abrir el archivo
	file.open(fileName.data(), ios::in | ios::out);
	if (!file.is_open()) // no existe
	{
		file.close();
		file.open(fileName.data(), ios::out); // crea el archivo
		file.close();
		file.open(fileName.data(), ios::in | ios::out); // vuelve a abrir el archivo, seguro de que existe
	};
	file.seekp(0, ios::end); // Esto prende el bit de EOF
	file.clear(); // Limpia los flags de error
	if (file.tellp() > 0) // Si no estoy en archivo vacio
		lastLsn = file.tellp() - (streampos) sizeof(int); // LSN del ultimo registro que esta en el archivo
	else
		// Archivo vacio
	{
		lastLsn = 0;
		
	}
//cout<<"lastLCN:"<<lastLsn<<" fail:"<<file.fail()<<" bad:"<<file.bad()<<" tellg:"<<file.tellg()<<endl;		
//if (file.fail() || file.bad())
//cout<<"murio MALL MALL "<<endl;		

	m_CurrentSize = 0;
}

//**********************************************************

LogCache::~LogCache()
{
	if (m_RecordBuffer.size() > 0) // Si quedaron cosas sin escribir
		Flush();
	file.close();
}

//**********************************************************

int LogCache::FlushIfNecessary()
{
	// TODO armar la politica de flush. Por ahora flushea siempre.
	return Flush();
}

//**********************************************************

int LogCache::Flush()
{
	// Lo que hace es ir sacando registros del buffer
	// y intenta guardarlos a disco. Si no puede, para y devuelve error.
	int result = NO_ERROR;
	while (m_RecordBuffer.size() > 0)
	{
		CLogRecord* rec = m_RecordBuffer.front();

		result = rec->appendToFile(file);
		if (IsSuccess(result))
		{
			m_RecordBuffer.pop_front();
			m_CurrentSize = m_CurrentSize - rec->getSize();
			delete rec;
		}
		else
		{
			break;
		};
	};
	if (m_CurrentSize < 0)
	{
		perror("Todo mal, dio m_CurrentSize < 0");
		return ERR_FLUSHING;
	}

	return result;
}

//**********************************************************

void LogCache::UpdateLsnPerTransaction(int & size) // TODO test
{
	lastLsn = m_RecordBuffer.back()->GetLSN() + size - sizeof(int);
	if (m_RecordBuffer.back()->EntryType() != CHECKPOINT)
	{
		CBasicOperationRecord *lBasicOp = reinterpret_cast<CBasicOperationRecord*> (m_RecordBuffer.back());
		if (lBasicOp->EntryType() == ROLLBACK || lBasicOp->EntryType() == COMMIT)
			lsnPerTransaction.erase(lBasicOp->TransactionID());
		else
			lsnPerTransaction[lBasicOp->TransactionID()] = lastLsn;
	}
}

//**********************************************************

int LogCache::WriteEntry(CLogRecord* reg)
{
	int size = reg->getSize(); // Tamanio del registro, en bytes
	file.seekp(0, ios_base::end); // Va al fin del archivo
	file.clear(); // Limpia los flags de error

	reg->SetLSN(((int) file.tellp()) + m_CurrentSize);
	if (file.tellp() > 0) // Archivo no vacio
		reg->LastLSN(lastLsn);
	else
		// Archivo vacio
		reg->LastLSN(0);

	if (reg->EntryType() != CHECKPOINT)
	{
		CBasicOperationRecord *lBasicOp = reinterpret_cast<CBasicOperationRecord*> (reg);
		if (lBasicOp)
		{
			if (lsnPerTransaction.count(lBasicOp->TransactionID()) > 0)
				lBasicOp->LastLSNSameTransaction(lsnPerTransaction[lBasicOp->TransactionID()]);
			else
				lBasicOp->LastLSNSameTransaction(-1);
		}
	}

	// Si los datos son mas grandes que el tamanio de la cache, aunque flushee, no
	// voy a poder guardarlos. Tengo que escribirlos directamente al disco, previo flush de lo que ya hay.
	if (size > CACHE_SIZE) // Caso muy improbable
	{
		//Si el Flush falla, no se puede guardar el registro a disco, asi que el WriteEntry va a fallar.
		if (IsSuccess(Flush()))
		{
			if (IsSuccess(reg->appendToFile(file)))
			{
				UpdateLsnPerTransaction(size);
				delete reg;
			}
			else // No pudo escribir reg en disco
			{
				return ERR_FILE_WRITING;
			}
		}
		else // No pudo hacer el flush previo a la escritura de reg
		{
			return ERR_FLUSHING;
		}
	}
	else // Caso probable
	{
		if ((CACHE_SIZE - m_CurrentSize) < size)// No hay espacio para escribir en cache
			if (!IsSuccess(Flush()))
				return ERR_FLUSHING;
		m_RecordBuffer.push_back(reg); // Flush dio success, pone reg en el buffer, que quedo vacio
		UpdateLsnPerTransaction(size);
		m_CurrentSize += size;
	}

	if (IsSuccess(FlushIfNecessary()))
		return NO_ERROR;
	else
		return ERR_FLUSHING;
}

//**********************************************************

void LogCache::GetOpenTransactionsInfo(CCheckPointRecord* reg)
{
	if (reg)
	{
		for (map<int, int>::iterator it = lsnPerTransaction.begin(); it != lsnPerTransaction.end(); it++)
			reg->addTransaction(it->first, it->second);
	}
}

//**********************************************************
//TODO: De aca para abajo habria que revisar los metodos. Me parece que estan bien, pero no se.

// Escribe en entry el tipo de registro
// Devuelve la posicion donde comienza el ultimo registro de log
streampos LogCache::GetLastEntryType(LogEntryType &entry)
{
	entry = NONE;
//	cout<<"fail:"<<file.fail()<<" bad:"<<file.bad()<<" tellg:"<<file.tellg()<<endl;		
	streampos iniPos = file.tellg();
//	cout<<"fail:"<<file.fail()<<" bad:"<<file.bad()<<" tellg:"<<file.tellg()<<endl;		
	int lastLSN;

	file.seekg(lastLsn);

//if (file.fail() || file.bad())
//	cout<<"murio MALL "<<endl;		

//cout<<"fail:"<<file.fail()<<" bad:"<<file.bad()<<" tellg:"<<file.tellg()<<endl;		
	file.read(reinterpret_cast<fstream::char_type*>(&lastLSN), sizeof(lastLSN));

//cout<<"fail:"<<file.fail()<<" bad:"<<file.bad()<<" Lsn:"<<lastLSN<<endl;		
	if (!file.fail() && !file.bad())
	{
		// voy al comienzo del utlimo reg
		if(lastLSN>0) //no es el primer registro
		{
			file.seekg(lastLSN + sizeof(int)); 
//			cout<<"NO ULTIMO"<<endl;
		}
		else
			file.seekg(lastLSN ); 
		if (!file.fail() && !file.bad())
		{
			
			file.read(reinterpret_cast<fstream::char_type*> (&entry), sizeof(LogEntryType)); // leo en entry type.
			if (!file.fail() && !file.bad())
			{
//				if(entry==INSERT)
//					cout<<"INSERT:"<<entry<<endl;
				file.seekg(iniPos);
				return lastLSN + sizeof(int);

			}
			else
			{
cout<<"murio 3"<<endl;	
				file.seekg(iniPos);
				return -1;
			}
		}
		else
		{
cout<<"murio 2"<<endl;	
			//cannot reach that position...
			file.seekg(iniPos);
			return ERR_FILE_READING;
		};
	}
	else
	{
cout<<"murio 1"<<endl;	
		file.seekg(iniPos);
		return ERR_FILE_READING;
	};
}
;

//**********************************************************

// Escribe en entry el tipo de registro que comienza en la posicion pos
// Devuelve la posicion donde comienza el registro de log anterior al que empieza en pos
streampos LogCache::GetPrevEntryType(streampos pos, LogEntryType &entry)
{
	entry = NONE;
	streampos iniPos = file.tellg();
	int lastLSN;

	file.seekg(pos - (streampos) sizeof(int));
	if (!file.fail() && !file.bad())
	{
		file.read(reinterpret_cast<fstream::char_type*> (&lastLSN), sizeof(lastLSN));
		if (!file.fail() && !file.bad())
		{
			file.seekg(lastLSN + sizeof(int));
			if (!file.fail() && !file.bad())
			{
				file.read(reinterpret_cast<fstream::char_type*> (&entry), sizeof(LogEntryType)); // leo en entry type.
				if (!file.fail() && !file.bad())
				{
					file.seekg(iniPos);
					return lastLSN + sizeof(int);
				}
				else
				{
					file.seekg(iniPos);
					return ERR_FILE_READING;
				};
			}
			else
			{
				file.seekg(iniPos);
				return ERR_FILE_READING;
			}
		}
		else
		{
			file.seekg(iniPos);
			return ERR_FILE_READING;
		}
	}
	else
	{
		//cannot reach that position...
		file.seekg(iniPos);
		return ERR_FILE_READING;
	}
}

// Escribe en entry el tipo de registro que comienza en la posicion pos
// Devuelve la posicion donde comienza el registro de log poeterior al que empieza en pos
streampos LogCache::GetNextEntryType(streampos pos, LogEntryType &entry)
{
	//TODO
	CLogRecord R;
	streampos iniPos = file.tellg();

	file.seekg(pos);
	if (!file.fail() && !file.bad())
	{
		int result = R.Load(file, false);//lee el registro (sin levantar los objetos en memoria
		if (IsSuccess(result))
		{
			entry=R.EntryType();
			file.seekg(iniPos);
			return pos + (streampos)R.getSize();
		}
		else
		{
			file.seekg(iniPos);
			return ERR_FILE_READING;
		}
	}	
	file.seekg(iniPos);
	return ERR_FILE_READING;
}

int LogCache::LoadReg(streampos pos,CLogRecord &rec, bool estados)
{
	//TODO
	streampos iniPos = file.tellg();

	file.seekg(pos);
	if (!file.fail() && !file.bad())
	{
		int result=rec.Load(file, estados);
		file.seekg(iniPos);
		if (IsSuccess(result))
		{
			return NO_ERROR;
		}
		else
		{
			return result;
		}
	}
	file.seekg(iniPos);
	return ERR_FILE_READING;
}

//**********************************************************

void LogCache::GetTransactionID(streampos pos, int& transactionID)
{
	transactionID = -1;
	streampos iniPos = file.tellp();

	file.seekp(pos + (streampos) OFFSET_TO_TRANSACTIONID);
	if (!file.fail() && !file.bad())
		file.read(reinterpret_cast<fstream::char_type*> (&transactionID), sizeof(transactionID)); // leo en entry type
}

//**********************************************************

// Devuelve, si existe, la posicion del registro de rollback de esta transaccion
int LogCache::FindRollback(int transactionID, CBasicOperationRecord* rec)
{
	LogEntryType entry = NONE;
	rec = 0;

	streampos pos = GetLastEntryType(entry);
	while (!rec && pos >= 0)
	{
		if (entry == ROLLBACK)
		{
			int tID = -1;
			GetTransactionID(pos, tID);
			if (tID == transactionID)
			{
				file.seekp(pos);
				if (!file.fail() && !file.bad())
				{
					rec = new CRollbackRecord();
					rec->Load(file, true);
				}
				else
					pos = -1;
			}
			else
				pos = GetPrevEntryType(pos, entry);
		}
		else
			pos = GetPrevEntryType(pos, entry);
	};
/*	if (pos == -1)
		return ERR_FILE_READING;
	else*/
		return pos;
}
;

//**********************************************************

// Devuelve, si existe, la posicion del registro de commit de esta transaccion
CCommitRecord* LogCache::FindCommit(int transactionID)
{
	LogEntryType entry = NONE;
	CCommitRecord *rec = 0;

	streampos pos = GetLastEntryType(entry);
	while (!rec && pos >= 0)
	{
		if (entry == COMMIT)
		{
			int tID = -1;
			GetTransactionID(pos, tID);
			if (tID == transactionID)
			{
				file.seekp(pos);
				if (!file.fail() && !file.bad())
				{
					rec = new CCommitRecord();
					rec->Load(file);
				}
				else
					pos = -1;
			}
			else
				pos = GetPrevEntryType(pos, entry);
		}
		else
			pos = GetPrevEntryType(pos, entry);
	};
	return rec;
}
;

//**********************************************************
// OJO! rec es parametro InOut
bool LogCache::GetPrevRecordOfSameTransaction(CBasicOperationRecord* &rec)
{
	LogEntryType entry = NONE;
	int lastLSNSameTransaction = rec->LastLSNSameTransaction();
	if (lastLSNSameTransaction != -1)
	{
		streampos pos = lastLSNSameTransaction + sizeof(int);
		file.read(reinterpret_cast<fstream::char_type*> (&entry), sizeof(entry));
		if (entry == INSERT)
		{
			file.seekp(pos);
			if (!file.fail() && !file.bad())
			{
				delete rec;
				rec = new CInsertRecord();
				rec->Load(file, true);
				return true;
			}
		}
		else if (entry == UPDATE)
		{
			file.seekp(pos);
			if (!file.fail() && !file.bad())
			{
				delete rec;
				rec = new CUpdateRecord();
				rec->Load(file, true);
				return true;
			}
		}
		else if (entry == DELETE)
		{
			file.seekp(pos);
			if (!file.fail() && !file.bad())
			{
				delete rec;
				rec = new CDeleteRecord();
				rec->Load(file, true);
				return true;
			}
		}
	}
	file.clear();
	return false;
}

//**********************************************************

size_t LogCache::GetFileSize()
{
	streampos lCurrentPos = file.tellp();

	file.seekp(0, ios_base::beg);
	size_t ini = file.tellp();
	file.seekp(0, ios_base::end);
	size_t end = file.tellp();

	file.seekp(lCurrentPos);
	return (end - ini);
}

//**********************************************************
int LogCache::CleanUpLog()
{
	/*	// Si un ID de transacci�n se re asigna, y �sta ultima transacci�n sigue
	 // abierta cuando se hizo el ultimo checkpoint, �ste m�todo no va a borrar
	 // la transacci�n anterior.

	 // Busco el ultimo checkpoint
	 LogEntryType et = NONE;
	 streampos pos   = GetLastEntryType(et);
	 while((et != CHECKPOINT)&&(pos >= 0))
	 pos = GetPrevEntryType(pos, et);

	 // Si encontr� un registro de Checkpoint
	 if ( pos >= 0)
	 {
	 CCheckPointRecord checkpoint(pos);
	 vector<streampos> recordsToSave;
	 // Me tengo que quedar solo con las transacciones que estaban
	 // abiertas al momento del Checkpoint y las que se crearon despues...

	 // vuelvo al final... y voy para atras registro por registro
	 streampos pos = GetLastEntryType(et);
	 while( pos >= 0)
	 {
	 if ((et == CHECKPOINT)&&(checkpoint.LSN() == pos))
	 {
	 recordsToSave.push_back(pos);
	 }else if (et == INSERT)
	 {
	 int transID = -1;
	 GetTransactionID(pos, transID);
	 CCheckPointRecord::CheckpointMap::iterator it = checkpoint.GetTransactionMap().find(transID);
	 if (it != checkpoint.GetTransactionMap().end() || pos > checkpoint.LSN())
	 recordsToSave.push_back(pos);

	 }else if (et == UPDATE)
	 {
	 int transID = -1;
	 GetTransactionID(pos, transID);
	 CCheckPointRecord::CheckpointMap::iterator it = checkpoint.GetTransactionMap().find(transID);
	 if (it != checkpoint.GetTransactionMap().end()|| pos > checkpoint.LSN())
	 recordsToSave.push_back(pos);

	 }else if (et == DELETE)
	 {
	 int transID = -1;
	 GetTransactionID(pos, transID);
	 CCheckPointRecord::CheckpointMap::iterator it = checkpoint.GetTransactionMap().find(transID);
	 if (it != checkpoint.GetTransactionMap().end()|| pos > checkpoint.LSN())
	 recordsToSave.push_back(pos);
	 }else if (et == ROLLBACK)
	 {
	 int transID = -1;
	 GetTransactionID(pos, transID);
	 CCheckPointRecord::CheckpointMap::iterator it = checkpoint.GetTransactionMap().find(transID);
	 if (it != checkpoint.GetTransactionMap().end()|| pos > checkpoint.LSN())
	 recordsToSave.push_back(pos);

	 }else if (et == COMMIT)
	 {
	 int transID = -1;
	 GetTransactionID(pos, transID);
	 CCheckPointRecord::CheckpointMap::iterator it = checkpoint.GetTransactionMap().find(transID);
	 if (it != checkpoint.GetTransactionMap().end()|| pos > checkpoint.LSN())
	 recordsToSave.push_back(pos);
	 };
	 pos = GetPrevEntryType(pos, et);
	 };

	 // Creo un archivo temporal
	 string tempName = fileName + ".tmp";
	 fstream ftemp;
	 ftemp.open(tempName.data(), ios::in | ios::out);

	 int lastLSN = 0;
	 map<int, int> LastLSNPerTransaction;
	 // Voy compiando solo los registro que salv�...
	 for (vector<streampos>::iterator it = recordsToSave.begin(); it != recordsToSave.end(); it++)
	 {
	 file.seekp(*it);
	 CLogRecord* record = CLogRecord::CreateRecord(file);
	 if (record)
	 {
	 //Reacomodo los LSN...
	 record->LSN(ftemp.tellp());
	 record->LastLSN(lastLSN);
	 if ((record->EntryType() == UPDATE)||(record->EntryType() == DELETE)||
	 (record->EntryType() == INSERT)||(record->EntryType() == COMMIT)||
	 (record->EntryType() == ROLLBACK))
	 {
	 CBasicOperationRecord *BRecord = reinterpret_cast<CBasicOperationRecord*>(record);
	 if (LastLSNPerTransaction.find(BRecord->TransactionID()) != LastLSNPerTransaction.end())
	 BRecord->LastLSNSameTransaction(LastLSNPerTransaction[BRecord->TransactionID()]);
	 else
	 BRecord->LastLSNSameTransaction(-1);
	 lastLSN = BRecord->LSN() + BRecord->getSize() - sizeof(int);
	 LastLSNPerTransaction[BRecord->TransactionID()] = lastLSN;
	 }else
	 lastLSN = record->LSN() + record->getSize() - sizeof(int);

	 record->appendToFile(ftemp);
	 delete record;
	 }else
	 {
	 //Aca que hago???
	 //Por algun motivo no pude obtener el registro...
	 };
	 };
	 LastLSNPerTransaction.clear();
	 recordsToSave.clear();

	 // Remplazo el archivo viejo...
	 ftemp.close();
	 file.close();
	 remove(fileName.data());
	 rename(tempName.data(), fileName.data());
	 // Reabro el archivo...
	 file.open(fileName.data(), ios::in | ios::out);
	 file.seekp(0, ios::end);
	 if (file.tellp() > 0)
	 lastLsn = file.tellp() - (streampos)sizeof(int);
	 else
	 lastLsn = 0;
	 }else
	 return 0; // Si no hay un Checkpoint no borro nada.*/
}
